# import os
#
# import numpy as np
# import pandas as pd
# import seaborn as sns
# from matplotlib import pyplot as plt, font_manager
# from sklearn.decomposition import PCA
# from sklearn.manifold import TSNE
# from sklearn.metrics import confusion_matrix
#
#
# def _get_times_new_roman_config():
#     """辅助函数：获取Times New Roman字体配置，失败则返回空字典（用默认字体）"""
#     target_font = "Times New Roman"
#     font_config = {}
#     try:
#         # 检查系统是否存在目标字体（用'family'参数匹配FontProperties）
#         font_props = font_manager.FontProperties(family=target_font)
#         font_path = font_manager.findfont(font_props)
#         # 验证字体路径是否包含目标字体特征（避免误匹配）
#         if target_font.lower() in font_path.lower() or "times" in font_path.lower():
#             font_config = {"family": target_font}  # 键为'family'，适配FontProperties
#             print(f"✅ 检测到Times New Roman字体，已应用")
#     except Exception as e:
#         # 捕获字体不存在、路径查找失败等异常，降级为默认字体
#         print(f"⚠️ 未检测到Times New Roman字体（错误：{str(e)[:50]}），使用系统默认字体")
#     return font_config
#
#
# def visualize_mean_features(encoding_path, csv_path, show_feature='标注类别名称',
#                             save_fig_path='combined_vis.jpg'):
#     """
#     修复size_max参数错误,将类别特征平均值作为虚拟样本加入原始特征一起降维可视化
#     额外修复：图例title_fontsize与title_fontproperties参数冲突，统一用FontProperties配置标题字体
#     """
#     # 1. 初始化字体配置（优先Times New Roman）
#     font_config = _get_times_new_roman_config()
#     text_font = font_config.get('family') if font_config else None
#
#     # 2. 加载数据
#     encoding_array = np.load(encoding_path, allow_pickle=True)
#     df = pd.read_csv(csv_path)
#
#     # 验证数据有效性
#     if len(encoding_array) != len(df):
#         raise ValueError("特征样本数与CSV行数不匹配")
#     for col in [show_feature, '图片路径']:
#         if col not in df.columns:
#             raise ValueError(f"CSV缺少必要列:{col}")
#
#     # 3. 计算每个类别的特征平均值（作为"虚拟样本"）
#     class_names = df[show_feature].unique().tolist()
#     n_classes = len(class_names)
#     print(f"计算 {n_classes} 个类别的特征平均值,作为虚拟样本加入原始特征...")
#
#     mean_feats_list = []
#     mean_labels = []
#     for cls in class_names:
#         cls_feats = encoding_array[df[show_feature] == cls]
#         cls_mean = np.mean(cls_feats, axis=0)
#         mean_feats_list.append(cls_mean)
#         mean_labels.append(cls)
#
#     mean_feats_array = np.array(mean_feats_list)
#     combined_feats = np.vstack([encoding_array, mean_feats_array])
#
#     # 4. 准备合并后的标签数据
#     original_labels = df[show_feature].tolist()
#     original_types = ['原始样本'] * len(original_labels)
#     original_names = df['图片路径'].apply(lambda x: x.split('/')[-1]).tolist()
#
#     mean_types = ['均值样本'] * n_classes
#     mean_names = [f"{cls}_均值" for cls in class_names]
#
#     combined_labels = original_labels + mean_labels
#     combined_types = original_types + mean_types
#     combined_names = original_names + mean_names
#
#     # 5. TSNE降维
#     print("对合并后的特征（原始样本+均值样本）进行TSNE降维...")
#     tsne = TSNE(n_components=2, max_iter=20000, random_state=42)
#     combined_tsne_2d = tsne.fit_transform(combined_feats)
#
#     # 6. 准备可视化数据
#     vis_df = pd.DataFrame({
#         'X': combined_tsne_2d[:, 0],
#         'Y': combined_tsne_2d[:, 1],
#         show_feature: combined_labels,
#         '样本类型': combined_types,
#         '名称': combined_names,
#         '尺寸数值': [1 if t == '原始样本' else 5 for t in combined_types]
#     })
#
#     # 7. 绘制静态图（修复图例参数冲突）
#     plt.figure(figsize=(14, 12), facecolor='white')
#     palette = sns.hls_palette(n_classes)
#     cls_color_map = {cls: palette[i] for i, cls in enumerate(class_names)}
#
#     # 绘制原始样本
#     original_mask = vis_df['样本类型'] == '原始样本'
#     for cls in class_names:
#         cls_mask = original_mask & (vis_df[show_feature] == cls)
#         plt.scatter(
#             vis_df.loc[cls_mask, 'X'],
#             vis_df.loc[cls_mask, 'Y'],
#             color=cls_color_map[cls],
#             alpha=0.4,
#             s=30,
#             marker='o',
#             label=cls
#         )
#
#     # 绘制均值样本+类别名称标注
#     mean_mask = vis_df['样本类型'] == '均值样本'
#     for cls in class_names:
#         cls_mean_mask = mean_mask & (vis_df[show_feature] == cls)
#         # 均值样本散点
#         plt.scatter(
#             vis_df.loc[cls_mean_mask, 'X'],
#             vis_df.loc[cls_mean_mask, 'Y'],
#             color=cls_color_map[cls],
#             s=400,
#             marker='*',
#             edgecolors='black',
#             linewidths=2,
#             zorder=10
#         )
#         # 类别名称文本（显式指定fontfamily）
#         plt.text(
#             vis_df.loc[cls_mean_mask, 'X'].values[0] + 3.0,
#             vis_df.loc[cls_mean_mask, 'Y'].values[0],
#             cls,
#             fontsize=12,
#             fontweight='bold',
#             color=cls_color_map[cls],
#             bbox=dict(facecolor='white', edgecolor='gray', pad=3, alpha=0.8),
#             fontfamily=text_font
#         )
#
#     # 图例（修复参数冲突：去掉title_fontsize，将标题字体大小合并到title_fontproperties）
#     plt.legend(
#         title=show_feature,
#         fontsize=10,  # 图例项的字体大小（非标题）
#         bbox_to_anchor=(1.05, 1),
#         # 关键修复：用title_fontproperties同时配置标题字体家族和大小，避免与title_fontsize冲突
#         title_fontproperties=font_manager.FontProperties(
#             family=text_font,
#             size=12  # 原title_fontsize=12的功能迁移到这里
#         ) if text_font else font_manager.FontProperties(size=12),
#         # 图例项的字体配置
#         prop=font_manager.FontProperties(family=text_font, size=10) if text_font else None
#     )
#
#     # 轴刻度（显式传递fontfamily）
#     plt.xticks([], fontfamily=text_font)
#     plt.yticks([], fontfamily=text_font)
#
#     plt.tight_layout()
#     plt.savefig(save_fig_path, dpi=500, bbox_inches='tight')
#     print(f"静态图已保存至: {save_fig_path}")
#     plt.close()
#
#
# def npy_dim_reduction_visualization(npy_dir, dim_method="pca", save_path=None):
#     """
#     对文件夹内的任意一维npy文件进行降维可视化（维度不固定，需所有样本维度一致）
#     """
#     # 1. 初始化字体配置（优先Times New Roman）
#     font_config = _get_times_new_roman_config()
#     text_font = font_config.get('family') if font_config else None
#
#     # 2. 读取所有npy文件
#     npy_files = [f for f in os.listdir(npy_dir) if f.endswith(".npy")]
#     if len(npy_files) == 0:
#         raise FileNotFoundError(f"文件夹 {npy_dir} 中未找到npy文件")
#     if len(npy_files) < 2:
#         raise ValueError(f"至少需要2个npy文件才能进行降维，当前仅找到 {len(npy_files)} 个")
#
#     data_list = []
#     file_labels = []
#     target_dim = None
#
#     for filename in npy_files:
#         file_path = os.path.join(npy_dir, filename)
#         try:
#             data = np.load(file_path, allow_pickle=True).squeeze()
#             if data.ndim != 1:
#                 raise ValueError(f"非一维数据：{filename} 维度为 {data.shape}（需一维数组）")
#
#             if target_dim is None:
#                 target_dim = len(data)
#                 print(f"检测到数据维度：{target_dim} 维（以 {filename} 为基准）")
#             else:
#                 if len(data) != target_dim:
#                     raise ValueError(
#                         f"维度不一致：{filename} 为 {len(data)} 维，需与基准维度 {target_dim} 一致"
#                     )
#
#             data_list.append(data)
#             file_labels.append(os.path.splitext(filename)[0])
#
#         except Exception as e:
#             raise RuntimeError(f"处理文件 {filename} 失败：{str(e)}")
#
#     data_matrix = np.array(data_list)
#     sample_num, feat_dim = data_matrix.shape
#     print(f"数据加载完成：共 {sample_num} 个样本，每个样本 {feat_dim} 维")
#
#     # 3. 降维处理
#     if dim_method.lower() == "pca":
#         reducer = PCA(n_components=2, random_state=42)
#         reduced_data = reducer.fit_transform(data_matrix)
#         title_suffix = f"PCA降维可视化（原维度：{feat_dim}）"
#         explained_var = reducer.explained_variance_ratio_
#         print(
#             f"PCA降维结果：维度1解释 {explained_var[0]:.3f} 方差，维度2解释 {explained_var[1]:.3f} 方差（累计：{np.sum(explained_var):.3f}）")
#
#     elif dim_method.lower() == "tsne":
#         perplexity = min(max(2, sample_num // 4), sample_num - 1)
#         reducer = TSNE(
#             n_components=2,
#             random_state=42,
#             perplexity=perplexity,
#             init="pca"
#         )
#         reduced_data = reducer.fit_transform(data_matrix)
#         title_suffix = f"t-SNE降维可视化（原维度：{feat_dim}，perplexity={perplexity}）"
#         print(f"t-SNE降维完成（自动设置perplexity={perplexity}，适配 {sample_num} 个样本）")
#
#     else:
#         raise ValueError(f"不支持的降维方法 {dim_method}，仅可选'pca'或'tsne'")
#
#     # 4. 可视化
#     fig, ax = plt.subplots(figsize=(10, 8))
#
#     scatter = ax.scatter(
#         reduced_data[:, 0],
#         reduced_data[:, 1],
#         c=range(sample_num),
#         cmap="tab10" if sample_num <= 10 else "viridis",
#         s=120 if sample_num <= 15 else 80,
#         alpha=0.8,
#         edgecolors="black",
#         linewidth=1
#     )
#
#     # 样本标签注释（显式传递fontfamily）
#     for i, (x, y, label) in enumerate(zip(reduced_data[:, 0], reduced_data[:, 1], file_labels)):
#         ax.annotate(
#             label,
#             xy=(x, y),
#             xytext=(6, 6) if sample_num <= 10 else (4, 4),
#             textcoords="offset points",
#             fontsize=10 if sample_num <= 10 else 8,
#             bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8),
#             fontfamily=text_font
#         )
#
#     # 轴标签（显式传递fontfamily）
#     ax.set_xlabel("dim 1", fontsize=12, fontfamily=text_font)
#     ax.set_ylabel("dim 2", fontsize=12, fontfamily=text_font)
#     ax.grid(True, alpha=0.3)
#
#     # 颜色条标签（显式传递fontfamily）
#     cbar = plt.colorbar(scatter, ax=ax)
#     cbar.set_label("sample index", fontsize=10, fontfamily=text_font)
#     cbar.set_ticks(range(sample_num))
#
#     plt.tight_layout()
#
#     if save_path is not None:
#         os.makedirs(os.path.dirname(save_path), exist_ok=True)
#         plt.savefig(save_path, dpi=300, bbox_inches="tight")
#         print(f"图片已保存至：{save_path}")
#     else:
#         plt.show()
#
#
# def plot_confusion_matrix(
#         y_true,
#         y_pred,
#         classes,
#         save_path='confusion_matrix.jpg',
#         normalize=False,
#         title='Confusion Matrix',
#         cmap=plt.cm.Blues
# ):
#     """
#     功能整合版混淆矩阵工具：
#     1. 计算混淆矩阵（支持原始计数/按真实类别归一化）；
#     2. 数值在格子中水平+垂直双居中，避免偏上；
#     3. 优先使用Times New Roman字体（系统存在则应用，否则保留默认）；
#     4. 自适应画布尺寸，无多余空白且标签完整显示。
#     """
#     # 1. 初始化字体配置（复用统一逻辑）
#     font_config = _get_times_new_roman_config()
#     text_font = font_config.get('family') if font_config else None
#
#     # 2. 计算混淆矩阵
#     cm = confusion_matrix(
#         y_true=y_true,
#         y_pred=y_pred,
#         labels=np.arange(len(classes))
#     )
#
#     # 3. 绘制混淆矩阵
#     base_size = 0.9 if len(classes) <= 6 else (0.7 if len(classes) <= 12 else 0.55)
#     fig = plt.figure(figsize=(len(classes) * base_size, len(classes) * base_size * 0.8))
#
#     if normalize:
#         cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]
#         print(f"📊 混淆矩阵已归一化（按真实类别）")
#
#     im = plt.imshow(cm, interpolation="nearest", cmap=cmap)
#     plt.colorbar(im)
#
#     # 标题（显式传递fontfamily）
#     if title:
#         plt.title(title, fontsize=12, fontfamily=text_font, pad=15)
#
#     # 轴刻度与类别标签（显式传递fontfamily）
#     tick_marks = np.arange(len(classes))
#     plt.xticks(
#         tick_marks, classes,
#         rotation=45 if len(classes) <= 11 else 90,
#         ha="center", fontsize=10, fontfamily=text_font
#     )
#     plt.yticks(tick_marks, classes, fontsize=10, fontfamily=text_font)
#
#     # 单元格数值（显式传递fontfamily）
#     fmt = ".2f" if normalize else "d"
#     thresh = cm.max() / 2.
#     for i, j in np.ndindex(cm.shape):
#         plt.text(
#             j, i, format(cm[i, j], fmt),
#             horizontalalignment="center",
#             verticalalignment="center",
#             color="white" if cm[i, j] > thresh else "black",
#             fontsize=10, fontfamily=text_font
#         )
#
#     # 轴标签（显式传递fontfamily）
#     plt.ylabel("True Label", fontsize=11, fontfamily=text_font, labelpad=10)
#     plt.xlabel("Predicted Label", fontsize=11, fontfamily=text_font, labelpad=10)
#
#     plt.subplots_adjust(left=0.12, right=0.95, bottom=0.15 if len(classes) <= 8 else 0.2, top=0.88)
#     plt.tight_layout()
#
#     plt.savefig(save_path, dpi=1000, bbox_inches="tight", bbox_extra_artists=[im])
#     plt.close(fig)
#     print(f"💾 混淆矩阵已保存至：{save_path}")

import os
from typing import Literal, Optional, List, Tuple

import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt, font_manager
from matplotlib.lines import Line2D
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
from sklearn.metrics import confusion_matrix

plt.rcParams["font.family"] = 'Times New Roman'
plt.rcParams['axes.unicode_minus'] = False


def visualize_mean_features(encoding_path, csv_path, show_feature='标注类别名称',
                            save_fig_path='combined_vis.jpg'):
    """
    修复size_max参数错误,将类别特征平均值作为虚拟样本加入原始特征一起降维可视化
    Fix size_max parameter error, add category feature averages as virtual samples to original features for dimensionality reduction visualization
    """
    # 1. 加载数据
    # 1. Load data
    encoding_array = np.load(encoding_path, allow_pickle=True)
    df = pd.read_csv(csv_path)

    # 验证数据有效性
    # Verify data validity
    if len(encoding_array) != len(df):
        raise ValueError(
            "特征样本数与CSV行数不匹配")  # The number of feature samples does not match the number of CSV rows
    for col in [show_feature, '图片路径']:
        if col not in df.columns:
            raise ValueError(f"CSV缺少必要列:{col}")  # CSV lacks necessary column: {col}

    # 2. 计算每个类别的特征平均值（作为"虚拟样本"）
    # 2. Calculate feature averages for each category (as "virtual samples")
    class_names = df[show_feature].unique().tolist()
    n_classes = len(class_names)
    print(
        f"计算 {n_classes} 个类别的特征平均值,作为虚拟样本加入原始特征...")  # Calculating feature averages for {n_classes} categories to add as virtual samples to original features...

    # 按类别分组求均值
    # Calculate averages by category group
    mean_feats_list = []
    mean_labels = []
    for cls in class_names:
        cls_feats = encoding_array[df[show_feature] == cls]
        cls_mean = np.mean(cls_feats, axis=0)
        mean_feats_list.append(cls_mean)
        mean_labels.append(cls)

    # 转换为数组并合并
    # Convert to array and merge
    mean_feats_array = np.array(mean_feats_list)
    combined_feats = np.vstack(
        [encoding_array, mean_feats_array])  # 合并原始特征与均值特征 (Merge original features with mean features)

    # 3. 准备合并后的标签数据
    # 3. Prepare merged label data
    original_labels = df[show_feature].tolist()
    original_types = ['原始样本'] * len(original_labels)  # '原始样本' corresponds to "Original Sample"
    original_names = df['图片路径'].apply(lambda x: x.split('/')[-1]).tolist()  # '图片路径' corresponds to "Image Path"

    mean_types = ['均值样本'] * n_classes  # '均值样本' corresponds to "Mean Sample"
    mean_names = [f"{cls}_均值" for cls in class_names]  # '_均值' corresponds to "_mean"

    combined_labels = original_labels + mean_labels
    combined_types = original_types + mean_types
    combined_names = original_names + mean_names

    # 4. 对合并后的特征进行TSNE降维
    # 4. Perform TSNE dimensionality reduction on combined features
    print(
        "对合并后的特征（原始样本+均值样本）进行TSNE降维...")  # Performing TSNE dimensionality reduction on combined features (original samples + mean samples)...
    tsne = TSNE(n_components=2, max_iter=20000,
                random_state=42)  # 注意:sklearn 1.5+用max_iter替代n_iter (Note: sklearn 1.5+ uses max_iter instead of n_iter)
    combined_tsne_2d = tsne.fit_transform(combined_feats)

    # 5. 准备可视化数据（新增尺寸数值列）
    # 5. Prepare visualization data (add size value column)
    vis_df = pd.DataFrame({
        'X': combined_tsne_2d[:, 0],
        'Y': combined_tsne_2d[:, 1],
        show_feature: combined_labels,
        '样本类型': combined_types,  # '样本类型' corresponds to "Sample Type"
        '名称': combined_names,  # '名称' corresponds to "Name"
        '尺寸数值': [1 if t == '原始样本' else 5 for t in combined_types]
        # 原始样本=1,均值样本=5 (Original sample=1, Mean sample=5)
    })

    # 6. 绘制静态图
    # 6. Draw static graph
    plt.figure(figsize=(14 * 0.7, 12 * 0.7), facecolor='white')
    palette = sns.hls_palette(n_classes)
    cls_color_map = {cls: palette[i] for i, cls in enumerate(class_names)}

    # 绘制原始样本
    # Draw original samples
    original_mask = vis_df['样本类型'] == '原始样本'
    for cls in class_names:
        cls_mask = original_mask & (vis_df[show_feature] == cls)
        plt.scatter(
            vis_df.loc[cls_mask, 'X'],
            vis_df.loc[cls_mask, 'Y'],
            color=cls_color_map[cls],
            alpha=0.4,
            s=30,
            marker='o',
            label=cls
        )

    # 绘制均值样本
    # Draw mean samples
    mean_mask = vis_df['样本类型'] == '均值样本'
    for i, cls in enumerate(class_names):
        cls_mean_mask = mean_mask & (vis_df[show_feature] == cls)
        plt.scatter(
            vis_df.loc[cls_mean_mask, 'X'],
            vis_df.loc[cls_mean_mask, 'Y'],
            color=cls_color_map[cls],
            s=400,
            marker='*',
            edgecolors='black',
            linewidths=2,
            zorder=10
        )

        # 在均值样本旁添加类别名称
        # Add category name next to mean sample
        plt.text(
            vis_df.loc[cls_mean_mask, 'X'].values[0] + 3.0,  # x轴偏移一点,避免重叠 (Offset x-axis slightly to avoid overlap)
            vis_df.loc[cls_mean_mask, 'Y'].values[0],  # y轴对齐 (Align with y-axis)
            cls,  # 类别名称 (Category name)
            fontsize=12,
            fontweight='bold',
            color=cls_color_map[cls],
            bbox=dict(facecolor='white', edgecolor='gray', pad=3, alpha=0.8)
            # 白色背景框增强可读性 (White background box enhances readability)
        )

    # 设置坐标轴标题
    plt.xlabel("t-SNE Dimension 1", fontsize=14)
    plt.ylabel("t-SNE Dimension 2", fontsize=14)

    # 将图例放在图片方框之外
    plt.legend(fontsize=10, title_fontsize=12,
               bbox_to_anchor=(1.01, 1), loc='upper left')

    plt.xticks([])
    plt.yticks([])
    plt.tight_layout()
    plt.savefig(save_fig_path, dpi=500, bbox_inches='tight')
    print(f"静态图已保存至: {save_fig_path}")  # Static image saved to: {save_fig_path}
    plt.close()


def visualize_features(
        encoding_path,
        csv_path,
        show_feature='标注类别名称',
        save_fig_path='combined_vis.jpg',
        visualization_type: Literal['original', 'combined', 'centers_only'] = 'combined'
):
    """
    先降维再计算低维中心点，支持三种可视化模式

    参数:
        encoding_path: 特征数据(npy文件)路径
        csv_path: 样本信息CSV文件路径
        show_feature: 用于分组显示的特征列名，默认为'标注类别名称'
        save_fig_path: 可视化结果保存路径
        visualization_type: 可视化类型
            - 'original': 仅显示原始数据点
            - 'combined': 显示原始数据点和中心点(默认)
            - 'centers_only': 仅显示中心点
    """
    # 1. 加载数据
    encoding_array = np.load(encoding_path, allow_pickle=True)
    df = pd.read_csv(csv_path)

    # 验证数据有效性
    if len(encoding_array) != len(df):
        raise ValueError("特征样本数与CSV行数不匹配")
    for col in [show_feature, '图片路径']:
        if col not in df.columns:
            raise ValueError(f"CSV缺少必要列:{col}")

    # 2. 对原始特征进行TSNE降维
    print("对原始特征进行TSNE降维...")
    tsne = TSNE(n_components=2, max_iter=20000, random_state=42)
    original_tsne_2d = tsne.fit_transform(encoding_array)  # 原始样本的低维坐标

    # 3. 构建原始样本的降维结果DataFrame
    tsne_df = pd.DataFrame({
        'X': original_tsne_2d[:, 0],
        'Y': original_tsne_2d[:, 1],
        show_feature: df[show_feature].tolist(),
        '图片路径': df['图片路径'].tolist()
    })

    # 4. 计算每个类别的低维中心点（坐标均值）
    class_names = df[show_feature].unique().tolist()
    class_centers = {}  # 存储每个类别的低维中心点 (x, y)
    for cls in class_names:
        cls_samples = tsne_df[tsne_df[show_feature] == cls]
        center_x = cls_samples['X'].mean()
        center_y = cls_samples['Y'].mean()
        class_centers[cls] = (center_x, center_y)

    # 5. 可视化设置
    plt.figure(figsize=(14 * 0.7, 12 * 0.7), facecolor='white')
    palette = sns.hls_palette(len(class_names))
    cls_color_map = {cls: palette[i] for i, cls in enumerate(class_names)}

    # 5.1 根据可视化类型绘制相应元素
    if visualization_type in ['original', 'combined']:
        # 绘制原始样本（透明小点，体现分布）
        for cls in class_names:
            cls_samples = tsne_df[tsne_df[show_feature] == cls]
            plt.scatter(
                cls_samples['X'], cls_samples['Y'],
                color=cls_color_map[cls],
                alpha=0.4,
                s=30,
                marker='o',
                label=cls
            )

    if visualization_type in ['centers_only', 'combined']:
        # 绘制低维中心点（大星号，突出中心）
        for cls, (cx, cy) in class_centers.items():
            plt.scatter(
                cx, cy,
                color=cls_color_map[cls],
                s=400,
                marker='*',
                edgecolors='black',
                linewidths=2,
                zorder=10  # 确保中心点在最上层
            )
            # 添加类别标签（带背景框增强可读性）
            plt.text(
                cx + 3.0, cy,  # x轴偏移避免重叠
                cls,
                fontsize=12,
                fontweight='bold',
                color=cls_color_map[cls],
                bbox=dict(facecolor='white', edgecolor='gray', pad=3, alpha=0.8)
            )

    # 6. 图表美化
    plt.xlabel("t-SNE Dimension 1", fontsize=14)
    plt.ylabel("t-SNE Dimension 2", fontsize=14)

    # 根据可视化类型调整图例
    if visualization_type != 'centers_only':
        plt.legend(fontsize=10, title_fontsize=12, bbox_to_anchor=(1.01, 1), loc='upper left')

    plt.xticks([])
    plt.yticks([])
    plt.tight_layout()
    plt.savefig(save_fig_path, dpi=500, bbox_inches='tight')
    print(f"可视化图已保存至: {save_fig_path}")
    plt.close()


# def visualize_multi_features(
#         encoding_paths: List[str] | Tuple[str],  # 特征文件路径列表
#         csv_paths: List[str] | Tuple[str],  # CSV文件路径列表（每个特征对应一个CSV）
#         show_features: List[str] | Tuple[str],  # 每个特征的分组列（与特征/CSV一一对应）
#         save_fig_path: str = 'multi_features_vis.jpg',
#         vis_types: List[Literal['original', 'combined', 'centers_only']] | Tuple[
#             Literal['original', 'combined', 'centers_only']] = None,
#         center_edges: List[str] | Tuple[str] = None,
#         tsne_random_state: int = 42,
#         default_center_edge: str = 'black'
# ):
#     """
#     多个特征文件分别对应一个CSV文件→统一TSNE降维，按需求绘制：
#     1. 原始数据：圆圈（o）标记，不同类别不同颜色
#     2. 中心点：星号（*）标记，与对应类别颜色一致
#     3. 图例：仅显示类别名称
#     """
#     # -------------------------- 1. 参数校验与预处理 --------------------------
#     n_features = len(encoding_paths)
#     if n_features == 0:
#         raise ValueError("至少需要1个特征文件")
#
#     # 校验CSV路径数量与特征数量一致
#     if len(csv_paths) != n_features:
#         raise ValueError(f"CSV文件数量（{len(csv_paths)}）必须与特征文件数量（{n_features}）一致")
#
#     # 校验show_features长度（与特征数量一致）
#     if len(show_features) != n_features:
#         raise ValueError(f"show_features长度（{len(show_features)}）必须与特征数量（{n_features}）一致")
#
#     # 处理vis_types默认值（全为'combined'）
#     if vis_types is None:
#         vis_types = ['combined'] * n_features
#     elif len(vis_types) != n_features:
#         raise ValueError(f"vis_types长度（{len(vis_types)}）必须与特征数量（{n_features}）一致")
#     # 校验可视化类型合法性
#     valid_vis = {'original', 'combined', 'centers_only'}
#     for i, vt in enumerate(vis_types):
#         if vt not in valid_vis:
#             raise ValueError(f"第{i + 1}个特征的可视化类型'{vt}'不合法，需为{valid_vis}之一")
#
#     # 处理center_edges默认值（默认黑色边框）
#     if center_edges is None:
#         center_edges = [default_center_edge] * n_features
#     elif len(center_edges) < n_features:
#         center_edges += [default_center_edge] * (n_features - len(center_edges))
#
#     # -------------------------- 2. 数据加载与特征堆叠（按数量拼接） --------------------------
#     # 加载所有CSV（每个特征对应一个CSV）
#     csv_dfs = []
#     for i, csv_path in enumerate(csv_paths):
#         try:
#             df = pd.read_csv(csv_path)
#         except Exception as e:
#             raise RuntimeError(f"加载第{i + 1}个CSV失败（{csv_path}）：{str(e)}")
#         csv_dfs.append(df)
#
#     # 记录每个特征的样本数（等于对应CSV的行数）
#     feat_sizes = [len(df) for df in csv_dfs]
#     total_samples = sum(feat_sizes)
#     if total_samples == 0:
#         raise ValueError("所有CSV文件均无数据")
#
#     # 校验每个CSV的必要列（对应特征的分组列）
#     for i in range(n_features):
#         col = show_features[i]
#         csv_path = csv_paths[i]
#         if col not in csv_dfs[i].columns:
#             raise ValueError(f"第{i + 1}个CSV（{csv_path}）缺少必要列: {col}")
#
#     # 加载所有特征并校验（样本数+维度）
#     features = []
#     for i, path in enumerate(encoding_paths):
#         try:
#             feat = np.load(path, allow_pickle=True)
#         except Exception as e:
#             raise RuntimeError(f"加载第{i + 1}个特征失败（{path}）：{str(e)}")
#         # 校验当前特征样本数与对应CSV行数一致
#         if len(feat) != feat_sizes[i]:
#             raise ValueError(f"第{i + 1}个特征样本数（{len(feat)}）必须=对应CSV行数（{feat_sizes[i]}）")
#         # 校验所有特征维度一致
#         if i > 0 and feat.shape[1] != features[0].shape[1]:
#             raise ValueError(f"第{i + 1}个特征维度（{feat.shape[1]}）必须与第1个特征维度（{features[0].shape[1]}）一致")
#         features.append(feat)
#
#     # 按数量拼接（样本堆叠）
#     combined_feat = np.concatenate(features, axis=0)
#     feat_dim = combined_feat.shape[1]
#     print(f"按数量拼接后特征形状: {combined_feat.shape}（总样本数：{total_samples}, 单个特征维度：{feat_dim}）")
#
#     # 记录每个特征的样本索引范围
#     feat_indices = []
#     start_idx = 0
#     for size in feat_sizes:
#         end_idx = start_idx + size
#         feat_indices.append((start_idx, end_idx))
#         start_idx = end_idx
#
#     # -------------------------- 3. 统一TSNE降维 --------------------------
#     print(f"对堆叠后的{total_samples}个样本进行TSNE降维...")
#     tsne = TSNE(
#         n_components=2,
#         max_iter=20000,
#         random_state=tsne_random_state,
#         perplexity=min(30, total_samples - 1)
#     )
#     shared_tsne_2d = tsne.fit_transform(combined_feat)
#
#     # -------------------------- 4. 全局颜色映射（按所有类别） --------------------------
#     # 收集所有特征的所有类别，生成唯一颜色（使用HUSL调色板增强区分度）
#     all_groups = []
#     for i in range(n_features):
#         col = show_features[i]
#         all_groups.extend(csv_dfs[i][col].unique().tolist())  # 每个特征用对应CSV的分组列
#     all_groups = list(set(all_groups))  # 去重，确保同一类别颜色唯一
#
#     # 核心修改：使用HUSL调色板（比HLS更均匀，区分度更高），并适当提高饱和度
#     # husl_palette参数：s=饱和度（0-1），l=亮度（0-1），默认s=0.7, l=0.6，可根据需求调整
#     palette = sns.husl_palette(
#         n_colors=len(all_groups),
#         s=0.8,  # 提高饱和度（0.8较适中，避免过饱和）
#         l=0.5  # 亮度适中（0.5避免过亮或过暗）
#     )
#     group_color_map = {group: palette[i] for i, group in enumerate(all_groups)}
#
#     # -------------------------- 5. 计算每个特征的中心点 --------------------------
#     feat_centers = []
#     for i in range(n_features):
#         col = show_features[i]
#         start_idx, end_idx = feat_indices[i]
#         feat_tsne = shared_tsne_2d[start_idx:end_idx]
#         centers = {}
#         # 使用当前特征对应的CSV计算中心点
#         for group in csv_dfs[i][col].unique():
#             csv_group_indices = csv_dfs[i][csv_dfs[i][col] == group].index
#             group_tsne = feat_tsne[csv_group_indices]
#             centers[group] = (group_tsne[:, 0].mean(), group_tsne[:, 1].mean())
#         feat_centers.append(centers)
#
#     # -------------------------- 6. 绘制（核心：按需求固定标记） --------------------------
#     plt.figure(figsize=(16 * 0.8, 14 * 0.8), facecolor='white')
#     ax = plt.gca()
#
#     # 用于记录已添加的图例标签（避免重复）
#     added_labels = set()
#
#     # 循环绘制每个特征
#     for i in range(n_features):
#         col = show_features[i]  # 当前特征的分组列
#         vis_type = vis_types[i]
#         center_edge = center_edges[i]
#         centers = feat_centers[i]
#         start_idx, end_idx = feat_indices[i]
#         feat_tsne = shared_tsne_2d[start_idx:end_idx]
#         current_df = csv_dfs[i]  # 当前特征对应的CSV数据
#
#         # 6.1 绘制原始数据（固定圆圈o，不同类别不同颜色）
#         if vis_type in ['original', 'combined']:
#             for group in current_df[col].unique():
#                 csv_group_indices = current_df[current_df[col] == group].index
#                 group_tsne = feat_tsne[csv_group_indices]
#                 # 图例标签：仅显示类别
#                 label = f"{group}"
#                 # 仅添加未出现过的标签，避免图例重复
#                 if label not in added_labels:
#                     ax.scatter(
#                         group_tsne[:, 0], group_tsne[:, 1],
#                         color=group_color_map[group],
#                         alpha=0.4,
#                         s=50,
#                         marker='o',  # 原始数据固定用圆圈
#                         label=label
#                     )
#                     added_labels.add(label)
#                 else:
#                     # 已添加过标签的，不重复加label参数
#                     ax.scatter(
#                         group_tsne[:, 0], group_tsne[:, 1],
#                         color=group_color_map[group],
#                         alpha=0.4,
#                         s=50,
#                         marker='o'
#                     )
#
#         # 6.2 绘制中心点（固定星号*，颜色与类别一致）
#         if vis_type in ['centers_only', 'combined']:
#             for group in current_df[col].unique():
#                 cx, cy = centers[group]
#                 ax.scatter(
#                     cx, cy,
#                     color=group_color_map[group],
#                     s=500,  # 星号放大，更醒目
#                     marker='*',  # 中心点固定用星号
#                     edgecolors=center_edge,
#                     linewidths=2,
#                     zorder=10  # 置顶，不被原始点遮挡
#                 )
#
#     # -------------------------- 7. 图表美化 --------------------------
#     # 添加分组标签（基于第一个特征的中心点）
#     first_centers = feat_centers[0]
#     for group, (cx, cy) in first_centers.items():
#         ax.text(
#             cx - 5, cy-14,
#             f"{group}",  # 文本标签与图例一致
#             fontsize=14,
#             fontweight='bold',
#             color=group_color_map[group],
#             bbox=dict(facecolor='white', edgecolor='gray', pad=2, alpha=0.8)
#         )
#
#     # 坐标轴与标题
#     ax.set_xlabel("t-SNE Dimension 1", fontsize=14)
#     ax.set_ylabel("t-SNE Dimension 2", fontsize=14)
#
#     # 图例（仅显示类别）
#     ax.legend(
#         fontsize=10,
#         title_fontsize=12,
#         bbox_to_anchor=(1.01, 1),
#         loc='upper left',
#         ncol=1
#     )
#
#     # 隐藏刻度
#     ax.set_xticks([])
#     ax.set_yticks([])
#
#     # 保存图片
#     plt.tight_layout()
#     plt.savefig(save_fig_path, dpi=500, bbox_inches='tight')
#     print(f"可视化结果已保存至：{save_fig_path}")
#     plt.close()
def visualize_multi_features(
        encoding_paths: List[str] | Tuple[str],  # 特征文件路径列表（与样本形状/CSV一一对应）
        csv_paths: List[str] | Tuple[str],  # CSV文件路径列表（每个特征对应一个CSV）
        show_features: List[str] | Tuple[str],  # 每个特征的分组列（与特征一一对应）
        sample_markers: List[str] | Tuple[str],  # 每个特征的样本形状（列表传入，如['o','s']）
        save_fig_path: str = 'multi_features_vis.jpg',
        save_csv_path: str = 'tsne_visualization_data.csv',  # 新增：CSV保存路径
        vis_types: List[Literal['original', 'combined', 'centers_only']] | Tuple[
            Literal['original', 'combined', 'centers_only']] = None,
        center_edges: List[str] | Tuple[str] = None,
        tsne_random_state: int = 42,
        default_center_edge: str = 'black'
):
    """
    多个特征文件分别对应一个CSV文件→统一TSNE降维，按需求绘制，并保存降维数据到CSV
    新增：将TSNE结果、样本信息、绘图样式写入CSV，支持后续复用
    """
    # -------------------------- 1. 参数校验与预处理（同原逻辑） --------------------------
    n_features = len(encoding_paths)
    if n_features == 0:
        raise ValueError("至少需要1个特征文件")

    # 校验样本形状列表长度与特征数量一致
    if len(sample_markers) != n_features:
        raise ValueError(f"sample_markers长度（{len(sample_markers)}）必须与特征数量（{n_features}）一致")
    # 校验样本形状合法性（matplotlib支持的marker类型）
    valid_markers = plt.Line2D.markers.keys()
    for i, mk in enumerate(sample_markers):
        if mk not in valid_markers:
            raise ValueError(f"第{i + 1}个特征的样本形状'{mk}'不合法，参考matplotlib valid markers")

    # 原有参数校验：CSV数量、show_features长度、vis_types、center_edges
    if len(csv_paths) != n_features:
        raise ValueError(f"CSV文件数量（{len(csv_paths)}）必须与特征数量（{n_features}）一致")
    if len(show_features) != n_features:
        raise ValueError(f"show_features长度（{len(show_features)}）必须与特征数量（{n_features}）一致")

    if vis_types is None:
        vis_types = ['combined'] * n_features
    elif len(vis_types) != n_features:
        raise ValueError(f"vis_types长度（{len(vis_types)}）必须与特征数量（{n_features}）一致")
    valid_vis = {'original', 'combined', 'centers_only'}
    for i, vt in enumerate(vis_types):
        if vt not in valid_vis:
            raise ValueError(f"第{i + 1}个特征的可视化类型'{vt}'不合法，需为{valid_vis}之一")

    if center_edges is None:
        center_edges = [default_center_edge] * n_features
    elif len(center_edges) < n_features:
        center_edges += [default_center_edge] * (n_features - len(center_edges))

    # -------------------------- 2. 数据加载与特征堆叠（同原逻辑） --------------------------
    # 加载所有CSV（每个特征对应一个CSV）
    csv_dfs = []
    for i, csv_path in enumerate(csv_paths):
        try:
            df = pd.read_csv(csv_path)
        except Exception as e:
            raise RuntimeError(f"加载第{i + 1}个CSV失败（{csv_path}）：{str(e)}")
        csv_dfs.append(df)

    # 记录每个特征的样本数（等于对应CSV的行数）
    feat_sizes = [len(df) for df in csv_dfs]
    total_samples = sum(feat_sizes)
    if total_samples == 0:
        raise ValueError("所有CSV文件均无数据")

    # 校验每个CSV的必要列（对应特征的分组列）
    for i in range(n_features):
        col = show_features[i]
        csv_path = csv_paths[i]
        if col not in csv_dfs[i].columns:
            raise ValueError(f"第{i + 1}个CSV（{csv_path}）缺少必要列: {col}")

    # 加载所有特征并校验（样本数+维度）
    features = []
    for i, path in enumerate(encoding_paths):
        try:
            feat = np.load(path, allow_pickle=True)
        except Exception as e:
            raise RuntimeError(f"加载第{i + 1}个特征失败（{path}）：{str(e)}")
        if len(feat) != feat_sizes[i]:
            raise ValueError(f"第{i + 1}个特征样本数（{len(feat)}）必须=对应CSV行数（{feat_sizes[i]}）")
        if i > 0 and feat.shape[1] != features[0].shape[1]:
            raise ValueError(f"第{i + 1}个特征维度（{feat.shape[1]}）必须与第1个特征维度（{features[0].shape[1]}）一致")
        features.append(feat)

    # 按数量拼接（样本堆叠）
    combined_feat = np.concatenate(features, axis=0)
    feat_dim = combined_feat.shape[1]
    print(f"按数量拼接后特征形状: {combined_feat.shape}（总样本数：{total_samples}, 单个特征维度：{feat_dim}）")

    # 记录每个特征的样本索引范围
    feat_indices = []
    start_idx = 0
    for size in feat_sizes:
        end_idx = start_idx + size
        feat_indices.append((start_idx, end_idx))
        start_idx = end_idx

    # -------------------------- 3. 统一TSNE降维（同原逻辑） --------------------------
    print(f"对堆叠后的{total_samples}个样本进行TSNE降维...")
    tsne = TSNE(
        n_components=2,
        max_iter=20000,
        random_state=tsne_random_state,
        perplexity=min(30, total_samples - 1)
    )
    shared_tsne_2d = tsne.fit_transform(combined_feat)

    # -------------------------- 4. 全局颜色映射（同原逻辑） --------------------------
    all_groups = []
    for i in range(n_features):
        col = show_features[i]
        all_groups.extend(csv_dfs[i][col].unique().tolist())
    all_groups = list(set(all_groups))  # 去重，确保同一类别颜色唯一
    palette = sns.husl_palette(n_colors=len(all_groups), s=0.8, l=0.5)
    group_color_map = {group: palette[i] for i, group in enumerate(all_groups)}

    # -------------------------- 5. 计算每个特征的中心点（同原逻辑） --------------------------
    feat_centers = []
    for i in range(n_features):
        col = show_features[i]
        start_idx, end_idx = feat_indices[i]
        feat_tsne = shared_tsne_2d[start_idx:end_idx]
        centers = {}
        for group in csv_dfs[i][col].unique():
            csv_group_indices = csv_dfs[i][csv_dfs[i][col] == group].index
            group_tsne = feat_tsne[csv_group_indices]
            centers[group] = (group_tsne[:, 0].mean(), group_tsne[:, 1].mean())
        feat_centers.append(centers)

    # -------------------------- 新增：6. 整理数据并保存到CSV --------------------------
    print(f"整理降维数据并保存到CSV：{save_csv_path}")
    visualization_data = []  # 存储所有样本和中心点数据

    # 6.1 保存样本数据（非中心点）
    for feat_idx in range(n_features):
        col = show_features[feat_idx]
        vis_type = vis_types[feat_idx]
        center_edge = center_edges[feat_idx]
        start_idx, end_idx = feat_indices[feat_idx]
        feat_tsne = shared_tsne_2d[start_idx:end_idx]
        current_df = csv_dfs[feat_idx]
        current_marker = sample_markers[feat_idx]

        # 遍历当前特征的每个样本
        for csv_idx in range(len(current_df)):
            group = current_df.iloc[csv_idx][col]
            tsne1, tsne2 = feat_tsne[csv_idx]
            visualization_data.append({
                "tsne_dim1": tsne1,
                "tsne_dim2": tsne2,
                "feature_idx": feat_idx,  # 特征索引（区分不同特征文件）
                "group": group,  # 样本类别
                "marker": current_marker,  # 样本形状
                "is_center": False,  # 是否为中心点（样本为False）
                "center_edge": center_edge,  # 中心点边缘颜色（样本可复用）
                "vis_type": vis_type,  # 该特征的可视化类型
                "color_hex": "#{:02x}{:02x}{:02x}".format(  # 颜色转为16进制（便于后续绘图）
                    int(group_color_map[group][0] * 255),
                    int(group_color_map[group][1] * 255),
                    int(group_color_map[group][2] * 255)
                )
            })

    # 6.2 保存中心点数据
    for feat_idx in range(n_features):
        col = show_features[feat_idx]
        vis_type = vis_types[feat_idx]
        center_edge = center_edges[feat_idx]
        centers = feat_centers[feat_idx]
        current_df = csv_dfs[feat_idx]

        # 遍历当前特征的每个中心点
        for group in current_df[col].unique():
            tsne1, tsne2 = centers[group]
            visualization_data.append({
                "tsne_dim1": tsne1,
                "tsne_dim2": tsne2,
                "feature_idx": feat_idx,
                "group": group,
                "marker": "*",  # 中心点固定为星号
                "is_center": True,  # 中心点标识
                "center_edge": center_edge,
                "vis_type": vis_type,
                "color_hex": "#{:02x}{:02x}{:02x}".format(
                    int(group_color_map[group][0] * 255),
                    int(group_color_map[group][1] * 255),
                    int(group_color_map[group][2] * 255)
                )
            })

    # 6.3 转为DataFrame并保存
    vis_df = pd.DataFrame(visualization_data)
    vis_df.to_csv(save_csv_path, index=False, encoding='utf-8')
    print(f"CSV保存完成！包含{len(vis_df)}条数据（样本+中心点）")

    # -------------------------- 7. 绘制（同原逻辑） --------------------------
    plt.figure(figsize=(16 * 0.8, 14 * 0.8), facecolor='white')
    ax = plt.gca()

    # 全局图例元素列表（收集所有「特征+类别」的唯一组合）
    global_legend_elements = []

    # 循环绘制每个特征
    for i in range(n_features):
        col = show_features[i]
        vis_type = vis_types[i]
        center_edge = center_edges[i]
        centers = feat_centers[i]
        start_idx, end_idx = feat_indices[i]
        feat_tsne = shared_tsne_2d[start_idx:end_idx]
        current_df = csv_dfs[i]
        current_marker = sample_markers[i]  # 当前特征的样本形状

        # 7.1 绘制当前特征的样本（收集全局图例元素）
        if vis_type in ['original', 'combined']:
            for group in current_df[col].unique():
                csv_group_indices = current_df[current_df[col] == group].index
                group_tsne = feat_tsne[csv_group_indices]
                color = group_color_map[group]
                unique_label = f"{group}"

                # 绘制样本点
                ax.scatter(
                    group_tsne[:, 0], group_tsne[:, 1],
                    color=color,
                    alpha=0.4,
                    s=50,
                    marker=current_marker,
                    label=unique_label
                )

                # 收集全局图例元素（避免重复）
                if not any(elem.get_label() == unique_label for elem in global_legend_elements):
                    legend_elem = Line2D(
                        [0], [0], marker=current_marker, color='w', markerfacecolor=color,
                        markersize=8, label=unique_label, alpha=0.4
                    )
                    global_legend_elements.append(legend_elem)

        # 7.2 绘制当前特征的中心点（固定*，不加入图例）
        if vis_type in ['centers_only', 'combined']:
            for group in current_df[col].unique():
                cx, cy = centers[group]
                ax.scatter(
                    cx, cy,
                    color=group_color_map[group],
                    s=500,
                    marker='*',
                    edgecolors=center_edge,
                    linewidths=2,
                    zorder=10
                )

    # -------------------------- 8. 绘制单个合并图例（同原逻辑） --------------------------
    if global_legend_elements:
        ax.legend(
            handles=global_legend_elements,
            fontsize=20,
            bbox_to_anchor=(1.01, 1),
            loc='upper left',
            ncol=1,
            frameon=True,
            title=None
        )

    # 添加中心点文本标注
    first_centers = feat_centers[0]
    for group, (cx, cy) in first_centers.items():
        ax.text(
            cx - 10, cy - 15,
            f"{group}",
            fontsize=20,
            fontweight='bold',
            color=group_color_map[group],
            bbox=dict(facecolor='white', edgecolor='gray', pad=2, alpha=0.8)
        )

    # 坐标轴设置
    ax.set_xlabel("t-SNE Dimension 1", fontsize=20)
    ax.set_ylabel("t-SNE Dimension 2", fontsize=20)
    ax.set_xticks([])
    ax.set_yticks([])

    # 保存图片
    plt.tight_layout()
    plt.savefig(save_fig_path, dpi=600, bbox_inches='tight')
    print(f"可视化结果已保存至：{save_fig_path}")
    plt.close()

def visualize_from_csv(
        csv_path: str,  # 第一步保存的TSNE数据CSV路径
        save_fig_path: str = 'multi_features_vis_from_csv.jpg',
        dpi=600,
        tsne_label_fontsize: int = 15,
        legend_fontsize: int = 14,
        annotation_fontsize: int = 15
):
    """
    从CSV文件读取TSNE降维数据，绘制与原函数一致的特征可视化图
    CSV需包含列：tsne_dim1, tsne_dim2, feature_idx, group, marker, is_center, center_edge, vis_type, color_hex
    """
    # -------------------------- 1. 加载并校验CSV数据 --------------------------
    try:
        vis_df = pd.read_csv(csv_path)
    except Exception as e:
        raise RuntimeError(f"加载CSV失败（{csv_path}）：{str(e)}")

    # 校验必要列
    required_cols = ["tsne_dim1", "tsne_dim2", "feature_idx", "group", "marker",
                     "is_center", "center_edge", "vis_type", "color_hex"]
    missing_cols = [col for col in required_cols if col not in vis_df.columns]
    if missing_cols:
        raise ValueError(f"CSV缺少必要列：{missing_cols}，请使用visualize_multi_features生成的CSV")

    # 数据预处理（确保is_center为布尔值）
    vis_df["is_center"] = vis_df["is_center"].astype(bool)
    # 获取唯一特征索引和可视化类型
    unique_feat_indices = vis_df["feature_idx"].unique()
    n_features = len(unique_feat_indices)
    if n_features == 0:
        raise ValueError("CSV中无有效特征数据")

    # -------------------------- 2. 初始化画布与全局配置 --------------------------
    plt.figure(figsize=(16 * 0.8, 14 * 0.8), facecolor='white')
    ax = plt.gca()

    # 全局图例元素列表（避免重复）
    global_legend_elements = []
    # 记录已处理的「group+marker」组合（避免图例重复）
    processed_legend_keys = set()

    # -------------------------- 3. 按特征分组绘制 --------------------------
    for feat_idx in unique_feat_indices:
        # 筛选当前特征的所有数据（样本+中心点）
        feat_data = vis_df[vis_df["feature_idx"] == feat_idx].copy()
        if feat_data.empty:
            print(f"警告：特征索引{feat_idx}无数据，跳过绘制")
            continue

        # 获取当前特征的配置（同一特征的vis_type、center_edge一致）
        vis_type = feat_data["vis_type"].iloc[0]
        center_edge = feat_data["center_edge"].iloc[0]
        # 获取当前特征的所有类别
        unique_groups = feat_data["group"].unique()

        # 3.1 绘制样本点（非中心点）
        if vis_type in ['original', 'combined']:
            sample_data = feat_data[feat_data["is_center"] == False]
            for group in unique_groups:
                group_sample_data = sample_data[sample_data["group"] == group]
                if group_sample_data.empty:
                    continue

                # 获取当前组的样式（同一组的marker、color_hex一致）
                marker = group_sample_data["marker"].iloc[0]
                color_hex = group_sample_data["color_hex"].iloc[0]
                unique_label = f"{group}"
                # 图例唯一键（避免同一「group+marker」重复）
                legend_key = f"{group}_{marker}"

                # 绘制样本点
                ax.scatter(
                    group_sample_data["tsne_dim1"],
                    group_sample_data["tsne_dim2"],
                    color=color_hex,
                    alpha=0.4,
                    s=50,
                    marker=marker,
                    label=unique_label
                )

                # 收集图例元素（未处理过的组合才添加）
                if legend_key not in processed_legend_keys:
                    legend_elem = Line2D(
                        [0], [0], marker=marker, color='w', markerfacecolor=color_hex,
                        markersize=8, label=unique_label, alpha=0.4
                    )
                    global_legend_elements.append(legend_elem)
                    processed_legend_keys.add(legend_key)

        # 3.2 绘制中心点（固定星号）
        if vis_type in ['centers_only', 'combined']:
            center_data = feat_data[feat_data["is_center"] == True]
            for group in unique_groups:
                group_center_data = center_data[center_data["group"] == group]
                if group_center_data.empty:
                    continue

                # 获取中心点样式
                color_hex = group_center_data["color_hex"].iloc[0]
                tsne1 = group_center_data["tsne_dim1"].iloc[0]
                tsne2 = group_center_data["tsne_dim2"].iloc[0]

                # 绘制中心点
                ax.scatter(
                    tsne1,
                    tsne2,
                    color=color_hex,
                    s=500,
                    marker='*',
                    edgecolors=center_edge,
                    linewidths=2,
                    zorder=10  # 置于顶层，避免被样本点遮挡
                )

    # -------------------------- 4. 绘制合并图例与标注 --------------------------
    # 4.1 绘制全局图例（固定右上角）
    if global_legend_elements:
        ax.legend(
            handles=global_legend_elements,
            fontsize=legend_fontsize,
            bbox_to_anchor=(1.01, 1),
            loc='upper left',
            ncol=1,
            frameon=True,
            title=None
        )

    # 4.2 添加中心点文本标注（取第一个特征的中心点，避免重复）
    first_feat_center_data = vis_df[
        (vis_df["feature_idx"] == unique_feat_indices[0]) &
        (vis_df["is_center"] == True)
    ].drop_duplicates(subset=["group"])  # 去重同一组的中心点

    for _, row in first_feat_center_data.iterrows():
        ax.text(
            row["tsne_dim1"] - 10,  # 向左偏移，避开星号
            row["tsne_dim2"] - 15,  # 向下偏移，避开星号
            f"{row['group']}",
            fontsize=annotation_fontsize,
            fontweight='bold',
            color=row["color_hex"],
            bbox=dict(facecolor='white', edgecolor='gray', pad=2, alpha=0.8)
        )

    # -------------------------- 5. 坐标轴与保存配置 --------------------------
    ax.set_xlabel("t-SNE Dimension 1", fontsize=tsne_label_fontsize)
    ax.set_ylabel("t-SNE Dimension 2", fontsize=tsne_label_fontsize)
    ax.set_xticks([])  # 隐藏x轴刻度
    ax.set_yticks([])  # 隐藏y轴刻度

    # 保存图片（预留边缘空间，避免图例截断）
    plt.tight_layout()
    plt.savefig(save_fig_path, dpi=dpi, bbox_inches='tight')
    print(f"从CSV绘制的可视化结果已保存至：{save_fig_path}")
    plt.close()
# def visualize_multi_features(
#         encoding_paths: List[str] | Tuple[str],  # 特征文件路径列表（与样本形状/CSV一一对应）
#         csv_paths: List[str] | Tuple[str],  # CSV文件路径列表（每个特征对应一个CSV）
#         show_features: List[str] | Tuple[str],  # 每个特征的分组列（与特征一一对应）
#         sample_markers: List[str] | Tuple[str],  # 每个特征的样本形状（列表传入，如['o','s']）
#         save_fig_path: str = 'multi_features_vis.jpg',
#         vis_types: List[Literal['original', 'combined', 'centers_only']] | Tuple[
#             Literal['original', 'combined', 'centers_only']] = None,
#         center_edges: List[str] | Tuple[str] = None,
#         tsne_random_state: int = 42,
#         default_center_edge: str = 'black'
# ):
#     """
#     多个特征文件分别对应一个CSV文件→统一TSNE降维，按需求绘制：
#     1. 原始数据：按sample_markers指定形状，不同类别不同颜色
#     2. 中心点：固定星号（*）标记，与对应类别颜色一致（不显示在图例中）
#     3. 图例：所有特征的「形状+类别」合并为一个，无多余标题，位置固定在右上角
#     """
#     # -------------------------- 1. 参数校验与预处理 --------------------------
#     n_features = len(encoding_paths)
#     if n_features == 0:
#         raise ValueError("至少需要1个特征文件")
#
#     # 校验样本形状列表长度与特征数量一致
#     if len(sample_markers) != n_features:
#         raise ValueError(f"sample_markers长度（{len(sample_markers)}）必须与特征数量（{n_features}）一致")
#     # 校验样本形状合法性（matplotlib支持的marker类型）
#     valid_markers = plt.Line2D.markers.keys()
#     for i, mk in enumerate(sample_markers):
#         if mk not in valid_markers:
#             raise ValueError(f"第{i + 1}个特征的样本形状'{mk}'不合法，参考matplotlib valid markers")
#
#     # 原有参数校验：CSV数量、show_features长度、vis_types、center_edges
#     if len(csv_paths) != n_features:
#         raise ValueError(f"CSV文件数量（{len(csv_paths)}）必须与特征数量（{n_features}）一致")
#     if len(show_features) != n_features:
#         raise ValueError(f"show_features长度（{len(show_features)}）必须与特征数量（{n_features}）一致")
#
#     if vis_types is None:
#         vis_types = ['combined'] * n_features
#     elif len(vis_types) != n_features:
#         raise ValueError(f"vis_types长度（{len(vis_types)}）必须与特征数量（{n_features}）一致")
#     valid_vis = {'original', 'combined', 'centers_only'}
#     for i, vt in enumerate(vis_types):
#         if vt not in valid_vis:
#             raise ValueError(f"第{i + 1}个特征的可视化类型'{vt}'不合法，需为{valid_vis}之一")
#
#     if center_edges is None:
#         center_edges = [default_center_edge] * n_features
#     elif len(center_edges) < n_features:
#         center_edges += [default_center_edge] * (n_features - len(center_edges))
#
#     # -------------------------- 2. 数据加载与特征堆叠（按数量拼接） --------------------------
#     # 加载所有CSV（每个特征对应一个CSV）
#     csv_dfs = []
#     for i, csv_path in enumerate(csv_paths):
#         try:
#             df = pd.read_csv(csv_path)
#         except Exception as e:
#             raise RuntimeError(f"加载第{i + 1}个CSV失败（{csv_path}）：{str(e)}")
#         csv_dfs.append(df)
#
#     # 记录每个特征的样本数（等于对应CSV的行数）
#     feat_sizes = [len(df) for df in csv_dfs]
#     total_samples = sum(feat_sizes)
#     if total_samples == 0:
#         raise ValueError("所有CSV文件均无数据")
#
#     # 校验每个CSV的必要列（对应特征的分组列）
#     for i in range(n_features):
#         col = show_features[i]
#         csv_path = csv_paths[i]
#         if col not in csv_dfs[i].columns:
#             raise ValueError(f"第{i + 1}个CSV（{csv_path}）缺少必要列: {col}")
#
#     # 加载所有特征并校验（样本数+维度）
#     features = []
#     for i, path in enumerate(encoding_paths):
#         try:
#             feat = np.load(path, allow_pickle=True)
#         except Exception as e:
#             raise RuntimeError(f"加载第{i + 1}个特征失败（{path}）：{str(e)}")
#         if len(feat) != feat_sizes[i]:
#             raise ValueError(f"第{i + 1}个特征样本数（{len(feat)}）必须=对应CSV行数（{feat_sizes[i]}）")
#         if i > 0 and feat.shape[1] != features[0].shape[1]:
#             raise ValueError(f"第{i + 1}个特征维度（{feat.shape[1]}）必须与第1个特征维度（{features[0].shape[1]}）一致")
#         features.append(feat)
#
#     # 按数量拼接（样本堆叠）
#     combined_feat = np.concatenate(features, axis=0)
#     feat_dim = combined_feat.shape[1]
#     print(f"按数量拼接后特征形状: {combined_feat.shape}（总样本数：{total_samples}, 单个特征维度：{feat_dim}）")
#
#     # 记录每个特征的样本索引范围
#     feat_indices = []
#     start_idx = 0
#     for size in feat_sizes:
#         end_idx = start_idx + size
#         feat_indices.append((start_idx, end_idx))
#         start_idx = end_idx
#
#     # -------------------------- 3. 统一TSNE降维 --------------------------
#     print(f"对堆叠后的{total_samples}个样本进行TSNE降维...")
#     tsne = TSNE(
#         n_components=2,
#         max_iter=20000,
#         random_state=tsne_random_state,
#         perplexity=min(30, total_samples - 1)
#     )
#     shared_tsne_2d = tsne.fit_transform(combined_feat)
#
#     # -------------------------- 4. 全局颜色映射（按所有类别） --------------------------
#     # 收集所有特征的所有类别，生成唯一颜色（HUSL调色板增强区分度）
#     all_groups = []
#     for i in range(n_features):
#         col = show_features[i]
#         all_groups.extend(csv_dfs[i][col].unique().tolist())
#     all_groups = list(set(all_groups))  # 去重，确保同一类别颜色唯一
#     palette = sns.husl_palette(n_colors=len(all_groups), s=0.8, l=0.5)
#     group_color_map = {group: palette[i] for i, group in enumerate(all_groups)}
#
#     # -------------------------- 5. 计算每个特征的中心点 --------------------------
#     feat_centers = []
#     for i in range(n_features):
#         col = show_features[i]
#         start_idx, end_idx = feat_indices[i]
#         feat_tsne = shared_tsne_2d[start_idx:end_idx]
#         centers = {}
#         for group in csv_dfs[i][col].unique():
#             csv_group_indices = csv_dfs[i][csv_dfs[i][col] == group].index
#             group_tsne = feat_tsne[csv_group_indices]
#             centers[group] = (group_tsne[:, 0].mean(), group_tsne[:, 1].mean())
#         feat_centers.append(centers)
#
#     # -------------------------- 6. 绘制（核心：合并图例元素收集） --------------------------
#     plt.figure(figsize=(16 * 0.8, 14 * 0.8), facecolor='white')  # 保留右侧空间，适配右上角图例
#     ax = plt.gca()
#
#     # 全局图例元素列表（收集所有「特征+类别」的唯一组合）
#     global_legend_elements = []
#
#     # 循环绘制每个特征
#     for i in range(n_features):
#         col = show_features[i]
#         vis_type = vis_types[i]
#         center_edge = center_edges[i]
#         centers = feat_centers[i]
#         start_idx, end_idx = feat_indices[i]
#         feat_tsne = shared_tsne_2d[start_idx:end_idx]
#         current_df = csv_dfs[i]
#         current_marker = sample_markers[i]  # 当前特征的样本形状
#
#         # 6.1 绘制当前特征的样本（收集全局图例元素）
#         if vis_type in ['original', 'combined']:
#             for group in current_df[col].unique():
#                 csv_group_indices = current_df[current_df[col] == group].index
#                 group_tsne = feat_tsne[csv_group_indices]
#                 color = group_color_map[group]
#                 # 图例标签：特征标识+类别（确保唯一）
#                 unique_label = f"{group}"
#
#                 # 绘制样本点
#                 ax.scatter(
#                     group_tsne[:, 0], group_tsne[:, 1],
#                     color=color,
#                     alpha=0.4,
#                     s=50,
#                     marker=current_marker,
#                     label=unique_label  # 临时label，用于图例去重
#                 )
#
#                 # 收集全局图例元素（避免同一「特征+类别」重复）
#                 if not any(elem.get_label() == unique_label for elem in global_legend_elements):
#                     legend_elem = plt.Line2D(
#                         [0], [0], marker=current_marker, color='w', markerfacecolor=color,
#                         markersize=8, label=unique_label, alpha=0.4  # 匹配样本点样式
#                     )
#                     global_legend_elements.append(legend_elem)
#
#         # 6.2 绘制当前特征的中心点（固定*，不加入图例）
#         if vis_type in ['centers_only', 'combined']:
#             for group in current_df[col].unique():
#                 cx, cy = centers[group]
#                 ax.scatter(
#                     cx, cy,
#                     color=group_color_map[group],
#                     s=500,
#                     marker='*',
#                     edgecolors=center_edge,
#                     linewidths=2,
#                     zorder=10
#                 )
#
#     # -------------------------- 7. 绘制单个合并图例（核心修改：固定右上角） --------------------------
#     if global_legend_elements:
#         ax.legend(
#             handles=global_legend_elements,
#             fontsize=14,
#             bbox_to_anchor=(1.01, 1),  # 锚点：X=0.98（靠近图右边缘），Y=0.98（靠近图上边缘）
#             loc='upper left',  # 图例自身的对齐方式：右上角对齐锚点
#             ncol=1,
#             frameon=True,
#             title=None  # 不显示图例标题
#         )
#
#     # 添加中心点文本标注（避开星号）
#     first_centers = feat_centers[0]
#     for group, (cx, cy) in first_centers.items():
#         ax.text(
#             cx - 10, cy - 15,
#             f"{group}",
#             fontsize=15,
#             fontweight='bold',
#             color=group_color_map[group],
#             bbox=dict(facecolor='white', edgecolor='gray', pad=2, alpha=0.8)
#         )
#
#     # 坐标轴设置
#     ax.set_xlabel("t-SNE Dimension 1", fontsize=15)
#     ax.set_ylabel("t-SNE Dimension 2", fontsize=15)
#     ax.set_xticks([])
#     ax.set_yticks([])
#
#     # 保存图片（预留边缘空间，避免图例截断）
#     plt.tight_layout()
#     plt.savefig(save_fig_path, dpi=600, bbox_inches='tight')
#     print(f"可视化结果已保存至：{save_fig_path}")
#     plt.close()


def npy_dim_reduction_visualization(npy_dir, dim_method="pca", save_path=None):
    """
    对文件夹内的任意一维npy文件进行降维可视化（维度不固定，需所有样本维度一致）

    参数:
        npy_dir: npy文件所在文件夹路径
        dim_method: 降维方法，可选"pca"（默认）或"tsne"
        save_path: 图片保存路径（如None则不保存，仅显示）
    """
    # 1. 读取所有npy文件，动态适配维度
    npy_files = [f for f in os.listdir(npy_dir) if f.endswith(".npy")]
    if len(npy_files) == 0:
        raise FileNotFoundError(f"文件夹 {npy_dir} 中未找到npy文件")
    if len(npy_files) < 2:
        raise ValueError(f"至少需要2个npy文件才能进行降维，当前仅找到 {len(npy_files)} 个")

    data_list = []
    file_labels = []
    target_dim = None

    for filename in npy_files:
        file_path = os.path.join(npy_dir, filename)
        try:
            data = np.load(file_path, allow_pickle=True).squeeze()
            if data.ndim != 1:
                raise ValueError(f"非一维数据：{filename} 维度为 {data.shape}（需一维数组）")

            if target_dim is None:
                target_dim = len(data)
                print(f"检测到数据维度：{target_dim} 维（以 {filename} 为基准）")
            else:
                if len(data) != target_dim:
                    raise ValueError(
                        f"维度不一致：{filename} 为 {len(data)} 维，需与基准维度 {target_dim} 一致"
                    )

            data_list.append(data)
            file_labels.append(os.path.splitext(filename)[0])

        except Exception as e:
            raise RuntimeError(f"处理文件 {filename} 失败：{str(e)}")

    data_matrix = np.array(data_list)
    sample_num, feat_dim = data_matrix.shape  # 这里定义了feat_dim（每个样本的维度）
    print(f"数据加载完成：共 {sample_num} 个样本，每个样本 {feat_dim} 维")

    # 2. 降维处理
    if dim_method.lower() == "pca":
        reducer = PCA(n_components=2, random_state=42)
        reduced_data = reducer.fit_transform(data_matrix)
        title_suffix = f"PCA降维可视化（原维度：{feat_dim}）"
        explained_var = reducer.explained_variance_ratio_
        print(
            f"PCA降维结果：维度1解释 {explained_var[0]:.3f} 方差，维度2解释 {explained_var[1]:.3f} 方差（累计：{np.sum(explained_var):.3f}）")

    elif dim_method.lower() == "tsne":
        perplexity = min(max(2, sample_num // 4), sample_num - 1)
        reducer = TSNE(
            n_components=2,
            random_state=42,
            perplexity=perplexity,
            init="pca"
        )
        reduced_data = reducer.fit_transform(data_matrix)
        title_suffix = f"t-SNE降维可视化（原维度：{feat_dim}，perplexity={perplexity}）"
        print(f"t-SNE降维完成（自动设置perplexity={perplexity}，适配 {sample_num} 个样本）")

    else:
        raise ValueError(f"不支持的降维方法 {dim_method}，仅可选'pca'或'tsne'")

    # 3. 可视化（修正变量名错误）
    # plt.rcParams["font.sans-serif"] = ["SimHei", "WenQuanYi Zen Hei"]
    # plt.rcParams["axes.unicode_minus"] = False

    fig, ax = plt.subplots(figsize=(10, 8))

    scatter = ax.scatter(
        reduced_data[:, 0],
        reduced_data[:, 1],
        c=range(sample_num),
        cmap="tab10" if sample_num <= 10 else "viridis",
        s=120 if sample_num <= 15 else 80,
        alpha=0.8,
        edgecolors="black",
        linewidth=1
    )

    for i, (x, y, label) in enumerate(zip(reduced_data[:, 0], reduced_data[:, 1], file_labels)):
        ax.annotate(
            label,
            xy=(x, y),
            xytext=(6, 6) if sample_num <= 10 else (4, 4),
            textcoords="offset points",
            fontsize=10 if sample_num <= 10 else 8,
            bbox=dict(boxstyle="round,pad=0.2", facecolor="white", alpha=0.8)
        )

    # 关键修正：将n_dim改为feat_dim（已定义的变量）
    # ax.set_title(f"{sample_num}个{feat_dim}维npy文件-{title_suffix}", fontsize=14, fontweight="bold")
    ax.set_xlabel("dim 1", fontsize=12)
    ax.set_ylabel("dim 2", fontsize=12)
    ax.grid(True, alpha=0.3)

    cbar = plt.colorbar(scatter, ax=ax)
    cbar.set_label("smple idex", fontsize=10)
    cbar.set_ticks(range(sample_num))

    plt.tight_layout()

    if save_path is not None:
        os.makedirs(os.path.dirname(save_path), exist_ok=True)
        plt.savefig(save_path, dpi=300, bbox_inches="tight")
        print(f"图片已保存至：{save_path}")
    else:
        plt.show()


def plot_confusion_matrix(
        y_true,
        y_pred,
        classes,
        save_path='confusion_matrix.jpg',
        normalize=False,
        title='Confusion Matrix',
        cmap=plt.cm.Blues
):
    """
    功能整合版混淆矩阵工具：
    1. 计算混淆矩阵（支持原始计数/按真实类别归一化）；
    2. 数值在格子中水平+垂直双居中，避免偏上；
    3. 优先使用Times New Roman字体（系统存在则应用，否则保留默认）；
    4. 自适应画布尺寸，无多余空白且标签完整显示。

    参数：
        y_true: 真实标签（array-like，形状(n_samples,)）
        y_pred: 预测标签（array-like，与y_true形状一致）
        classes: 类别名称列表（list，与标签索引一一对应）
        save_path: 图像保存路径（默认'confusion_matrix.jpg'）
        normalize: 是否按真实类别（行）归一化（默认False）
        title: 混淆矩阵标题（默认'Confusion Matrix'）
        cmap: 颜色映射（默认plt.cm.Blues，可换'viridis'等）

    返回：
        np.ndarray: 计算后的混淆矩阵（原始/归一化）
    """

    # -------------------------- 2. 计算混淆矩阵（确保类别顺序与classes一致） --------------------------
    cm = confusion_matrix(
        y_true=y_true,
        y_pred=y_pred,
        labels=np.arange(len(classes))  # 标签索引匹配classes长度
    )

    # -------------------------- 3. 绘制混淆矩阵（整合所有优化） --------------------------
    # 自适应画布：类别越多，单位尺寸越小，避免整体过大/过小
    base_size = 0.9 if len(classes) <= 6 else (0.7 if len(classes) <= 12 else 0.55)
    fig = plt.figure(figsize=(len(classes) * base_size, len(classes) * base_size * 0.8))

    # 归一化处理（按真实类别行求和归一化）
    if normalize:
        cm = cm.astype("float") / cm.sum(axis=1)[:, np.newaxis]
        print(f"📊 混淆矩阵已归一化（按真实类别）")

    # 绘制矩阵主体
    im = plt.imshow(cm, interpolation="nearest", cmap=cmap)
    plt.colorbar(im)  # 颜色条（辅助理解数值与颜色对应）

    # 标题：应用字体配置，控制字体大小
    if title:
        plt.title(title, fontsize=12, pad=15)

    # 刻度与类别标签：旋转X轴标签避免重叠，应用字体
    tick_marks = np.arange(len(classes))
    plt.xticks(
        tick_marks, classes,
        rotation=45 if len(classes) <= 11 else 90,  # 类别多则竖排标签
        ha="center", fontsize=10
    )
    plt.yticks(tick_marks, classes, fontsize=10)

    # 单元格数值：水平+垂直双居中，按背景色适配文字色
    fmt = ".2f" if normalize else "d"  # 归一化显示2位小数，原始显示整数
    thresh = cm.max() / 2.  # 阈值：区分深色/浅色背景
    for i, j in np.ndindex(cm.shape):
        plt.text(
            j, i, format(cm[i, j], fmt),
            horizontalalignment="center",  # 水平居中
            verticalalignment="center",  # 垂直居中（解决偏上问题）
            color="white" if cm[i, j] > thresh else "black",
            fontsize=10  # 数值字体稍小，避免拥挤
        )

    # 轴标签：应用字体，调整位置避免截断
    plt.ylabel("True Label", fontsize=11, labelpad=10)
    plt.xlabel("Predicted Label", fontsize=11, labelpad=10)

    # 精准调整边距：确保无多余空白，标签完整
    plt.subplots_adjust(left=0.12, right=0.95, bottom=0.15 if len(classes) <= 8 else 0.2, top=0.88)
    plt.tight_layout()

    # 保存高分辨率图像（自动裁剪边缘空白）
    plt.savefig(save_path, dpi=500, bbox_inches="tight", bbox_extra_artists=[im])
    plt.close(fig)
    print(f"💾 混淆矩阵已保存至：{save_path}")
